import os
import subprocess
from typing import List, Optional

from PySide6 import QtCore, QtGui, QtWidgets


def markdown_to_html(text: str) -> str:
    doc = QtGui.QTextDocument()
    doc.setMarkdown(text or "")
    return doc.toHtml()

from src.db import (
    add_tags_for_pdf,
    add_or_update_path,
    delete_tag,
    get_conn,
    get_pdf_tags,
    init_db,
    list_all_tags_with_counts,
    list_pdfs,
    query_pdf_ids_by_tag,
    query_pdfs_by_ids,
    remove_tags_for_pdf,
    update_pdf_description,
)
from src.scanner import scan_directory
from src.search import search_pdf_ids


class TagListWidget(QtWidgets.QListWidget):
    tagsChanged = QtCore.Signal()
    tagDeleteRequested = QtCore.Signal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
        self.itemChanged.connect(lambda _: self.tagsChanged.emit())
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self._on_context_menu)

    def refresh(self, conn):
        self.blockSignals(True)
        try:
            self.clear()
            for row in list_all_tags_with_counts(conn):
                name = row[0]
                cnt = row[1]
                item = QtWidgets.QListWidgetItem(f"{name} ({cnt})")
                item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
                item.setCheckState(QtCore.Qt.Unchecked)
                # 存储实际标签名
                item.setData(QtCore.Qt.UserRole, name)
                self.addItem(item)
        finally:
            self.blockSignals(False)

    def selected_tags(self) -> List[str]:
        tags: List[str] = []
        for i in range(self.count()):
            it = self.item(i)
            if it.checkState() == QtCore.Qt.Checked:
                tags.append(str(it.data(QtCore.Qt.UserRole)))
        return tags

    def _on_context_menu(self, pos: QtCore.QPoint) -> None:
        item = self.itemAt(pos)
        if item is None:
            return
        tag_name = item.data(QtCore.Qt.UserRole)
        if not tag_name:
            return
        menu = QtWidgets.QMenu(self)
        act_delete = menu.addAction("删除标签")
        action = menu.exec(self.mapToGlobal(pos))
        if action == act_delete:
            self.tagDeleteRequested.emit(str(tag_name))


class MarkdownDelegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, column: int, parent=None):
        super().__init__(parent)
        self.column = column

    def paint(
        self,
        painter: QtGui.QPainter,
        option: QtWidgets.QStyleOptionViewItem,
        index: QtCore.QModelIndex,
    ) -> None:
        if index.column() != self.column:
            super().paint(painter, option, index)
            return

        markdown_text = index.data(QtCore.Qt.DisplayRole)
        if not markdown_text:
            super().paint(painter, option, index)
            return

        widget = option.widget
        style = widget.style() if widget else QtWidgets.QApplication.style()

        opt = QtWidgets.QStyleOptionViewItem(option)
        self.initStyleOption(opt, index)
        opt.text = ""
        style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, opt, painter, widget)

        doc = QtGui.QTextDocument()
        doc.setDefaultFont(opt.font)
        doc.setMarkdown(str(markdown_text))
        doc.setTextWidth(opt.rect.width())

        painter.save()
        painter.translate(opt.rect.topLeft())
        clip_rect = QtCore.QRectF(0, 0, opt.rect.width(), opt.rect.height())
        painter.setClipRect(clip_rect)

        ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
        ctx.palette = option.palette
        if option.state & QtWidgets.QStyle.State_Selected:
            ctx.palette.setColor(
                QtGui.QPalette.Text,
                option.palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText),
            )
        doc.documentLayout().draw(painter, ctx)
        painter.restore()

    def sizeHint(
        self,
        option: QtWidgets.QStyleOptionViewItem,
        index: QtCore.QModelIndex,
    ) -> QtCore.QSize:
        if index.column() != self.column:
            return super().sizeHint(option, index)

        markdown_text = index.data(QtCore.Qt.DisplayRole)
        if not markdown_text:
            return super().sizeHint(option, index)

        doc = QtGui.QTextDocument()
        doc.setDefaultFont(option.font)
        doc.setMarkdown(str(markdown_text))
        width = option.rect.width()
        if width <= 0 and option.widget:
            try:
                width = option.widget.columnWidth(self.column)  # type: ignore[attr-defined]
            except Exception:
                width = 0
        if width > 0:
            doc.setTextWidth(width)
        size = doc.size().toSize()
        base = super().sizeHint(option, index)
        height = max(base.height(), size.height())
        return QtCore.QSize(base.width(), height)


class PdfTree(QtWidgets.QTreeWidget):
    openRequested = QtCore.Signal(str)
    renameRequested = QtCore.Signal(int, str)
    addTagsRequested = QtCore.Signal(list)
    removeTagsRequested = QtCore.Signal(list)
    removeFromDbRequested = QtCore.Signal(int)
    editDescriptionRequested = QtCore.Signal(list)
    viewDescriptionRequested = QtCore.Signal(int)
    exportDirectoryRequested = QtCore.Signal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setColumnCount(5)
        self.setHeaderLabels(["名称", "标签", "描述", "页数", "大小"])
        self.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
        self.header().setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
        self.header().setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
        self.header().setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
        self.header().setSectionResizeMode(4, QtWidgets.QHeaderView.ResizeToContents)
        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        self.setAlternatingRowColors(True)
        self.setUniformRowHeights(True)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self._on_context_menu)
        self.itemDoubleClicked.connect(self._on_double_click)
        self.setMouseTracking(True)

        self.setItemDelegateForColumn(2, MarkdownDelegate(column=2, parent=self))

    def _format_size(self, size_val: int) -> str:
        try:
            size = float(size_val)
        except Exception:
            return str(size_val)
        units = ["B", "KB", "MB", "GB", "TB"]
        i = 0
        while size >= 1024 and i < len(units) - 1:
            size /= 1024.0
            i += 1
        if i == 0:
            return f"{int(size)} {units[i]}"
        return f"{size:.2f} {units[i]}"

    def _truncate_description(self, desc: str, max_len: int = 50) -> str:
        if not desc:
            return ""
        first_line = desc.splitlines()[0]
        if len(first_line) <= max_len:
            return first_line
        return first_line[: max_len - 3] + "..."

    def populate_tree(self, rows):
        # rows: columns -> 0:id 1:page_count 2:size 3:file_mtime 4:description 5:path 6:tags
        self.clear()
        dir_to_parent = {}
        folder_icon = self.style().standardIcon(QtWidgets.QStyle.SP_DirIcon)
        file_icon = self.style().standardIcon(QtWidgets.QStyle.SP_FileIcon)
        for row in rows:
            pdf_id = int(row[0])
            path = row[5] or ""
            if not path:
                continue
            tags = row[6] or ""
            description = row[4] or ""
            description_short = self._truncate_description(description)
            page_count = str(row[1] or "")
            size = self._format_size(int(row[2] or 0))
            directory = os.path.dirname(path)
            filename = os.path.basename(path)
            parent = dir_to_parent.get(directory)
            if parent is None:
                parent = QtWidgets.QTreeWidgetItem([directory, "", "", "", ""]) if directory else QtWidgets.QTreeWidgetItem(["(无目录)", "", "", "", ""]) 
                parent.setFirstColumnSpanned(True)
                parent.setData(0, QtCore.Qt.UserRole, None)
                parent.setIcon(0, folder_icon)
                font = parent.font(0)
                font.setBold(True)
                parent.setFont(0, font)
                self.addTopLevelItem(parent)
            child = QtWidgets.QTreeWidgetItem([filename, tags, description_short, page_count, size])
            child.setData(0, QtCore.Qt.UserRole, pdf_id)
            child.setData(2, QtCore.Qt.UserRole, description)  # 存储完整描述
            child.setIcon(0, file_icon)
            first_line_full = description.splitlines()[0] if description else ""
            if first_line_full:
                child.setData(2, QtCore.Qt.ToolTipRole, first_line_full)
            parent.addChild(child)
            dir_to_parent[directory] = parent
        self.expandToDepth(0)

    def current_file_info(self) -> Optional[tuple[int, str]]:
        it = self.currentItem()
        if not it:
            return None
        pdf_id = it.data(0, QtCore.Qt.UserRole)
        if not pdf_id:
            return None
        # reconstruct full path from parent
        parent = it.parent()
        directory = parent.text(0) if parent else ""
        filename = it.text(0)
        fullpath = os.path.join(directory, filename) if directory not in ("", "(无目录)") else it.text(0)
        return int(pdf_id), fullpath

    def selected_pdf_ids(self) -> List[int]:
        """获取所有选中的PDF ID列表"""
        selected_items = self.selectedItems()
        pdf_ids = []
        for item in selected_items:
            pdf_id = item.data(0, QtCore.Qt.UserRole)
            if pdf_id:
                pdf_ids.append(int(pdf_id))
        return pdf_ids

    def get_item_by_id(self, pdf_id: int) -> Optional[QtWidgets.QTreeWidgetItem]:
        for i in range(self.topLevelItemCount()):
            parent = self.topLevelItem(i)
            for j in range(parent.childCount()):
                child = parent.child(j)
                if child.data(0, QtCore.Qt.UserRole) == pdf_id:
                    return child
        return None

    def _get_file_info_by_id(self, pdf_id: int) -> Optional[tuple[int, str]]:
        """根据PDF ID获取文件信息"""
        child = self.get_item_by_id(pdf_id)
        if child:
            parent = child.parent()
            directory = parent.text(0) if parent else ""
            filename = child.text(0)
            fullpath = os.path.join(directory, filename) if directory not in ("", "(无目录)") else child.text(0)
            return pdf_id, fullpath
        return None

    def _on_double_click(self, item, column):
        info = self.current_file_info()
        if info:
            _, path = info
            if path:
                self.openRequested.emit(path)
        else:
            # 双击文件夹
            directory = item.text(0)
            if directory and directory != "(无目录)":
                self.openRequested.emit(directory)

    def _on_context_menu(self, pos):
        item = self.itemAt(pos)
        if not item:
            return

        pdf_id = item.data(0, QtCore.Qt.UserRole)
        if not pdf_id:
            directory = item.text(0)
            if not directory or directory == "(无目录)":
                return

            menu = QtWidgets.QMenu(self)
            act_open_dir = menu.addAction("打开目录")
            act_export_dir = menu.addAction("导出目录标签")
            action = menu.exec(self.viewport().mapToGlobal(pos))
            if action == act_open_dir:
                self.openRequested.emit(directory)
            elif action == act_export_dir:
                self.exportDirectoryRequested.emit(directory)
            return

        selected_ids = self.selected_pdf_ids()
        if not selected_ids:
            selected_ids = [int(pdf_id)]

        menu = QtWidgets.QMenu(self)
        act_open = menu.addAction("打开")
        act_rename = menu.addAction("重命名…")
        act_add = menu.addAction("添加标签…")
        act_remove = menu.addAction("移除标签…")
        act_view = menu.addAction("查看完整描述…")
        act_desc = menu.addAction("编辑描述…")
        menu.addSeparator()
        act_drop = menu.addAction("从库移除（不删文件）")
        action = menu.exec(self.viewport().mapToGlobal(pos))
        if action == act_open:
            # 打开所有选中的PDF
            for each_id in selected_ids:
                info = self._get_file_info_by_id(each_id)
                if info:
                    _, path = info
                    if path:
                        self.openRequested.emit(path)
        elif action == act_rename:
            # 只重命名第一个选中的PDF
            info = self._get_file_info_by_id(selected_ids[0])
            if info:
                pdf_id_val, path = info
                self.renameRequested.emit(pdf_id_val, path)
        elif action == act_add:
            self.addTagsRequested.emit(selected_ids)
        elif action == act_remove:
            self.removeTagsRequested.emit(selected_ids)
        elif action == act_view:
            self.viewDescriptionRequested.emit(selected_ids[0])
        elif action == act_desc:
            self.editDescriptionRequested.emit(selected_ids)
        elif action == act_drop:
            for each_id in selected_ids:
                self.removeFromDbRequested.emit(each_id)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, db_path: str):
        super().__init__()
        self.db_path = db_path
        init_db(self.db_path)
        self.conn = get_conn(self.db_path)
        self.setWindowTitle("PDFer - 标签管理器")
        self.resize(1000, 640)

        # Widgets
        # 顶部筛选区：AND/OR 切换 + 应用筛选
        self.mode_combo = QtWidgets.QComboBox()
        self.mode_combo.addItems(["AND", "OR"])
        self.expr_input = QtWidgets.QLineEdit()
        self.expr_input.setPlaceholderText("布尔表达式，例如：tag1 AND (tag2 OR \"tag 3\")")
        self.apply_filter_btn = QtWidgets.QPushButton("应用筛选")
        self.scan_btn = QtWidgets.QPushButton("扫描目录")
        self.export_btn = QtWidgets.QPushButton("导出目录标签")
        self.add_tag_btn = QtWidgets.QPushButton("添加标签")
        self.remove_tag_btn = QtWidgets.QPushButton("移除标签")
        self.open_btn = QtWidgets.QPushButton("打开")

        top_bar = QtWidgets.QWidget()
        top_layout = QtWidgets.QHBoxLayout(top_bar)
        top_layout.addWidget(QtWidgets.QLabel("模式:"))
        top_layout.addWidget(self.mode_combo)
        top_layout.addWidget(QtWidgets.QLabel("布尔筛选:"))
        top_layout.addWidget(self.expr_input, 1)
        top_layout.addWidget(self.apply_filter_btn)
        top_layout.addWidget(self.scan_btn)
        top_layout.addWidget(self.export_btn)
        top_layout.addWidget(self.add_tag_btn)
        top_layout.addWidget(self.remove_tag_btn)
        top_layout.addWidget(self.open_btn)

        self.tag_list = TagListWidget()
        self.tree = PdfTree()

        splitter = QtWidgets.QSplitter()
        splitter.addWidget(self.tag_list)
        splitter.addWidget(self.tree)
        splitter.setStretchFactor(0, 0)
        splitter.setStretchFactor(1, 1)

        central = QtWidgets.QWidget()
        vbox = QtWidgets.QVBoxLayout(central)
        vbox.addWidget(top_bar)
        vbox.addWidget(splitter, 1)
        self.setCentralWidget(central)

        # Signals
        self.apply_filter_btn.clicked.connect(self.on_apply_filter)
        self.mode_combo.currentTextChanged.connect(self.on_apply_filter)
        self.scan_btn.clicked.connect(self.on_scan)
        self.export_btn.clicked.connect(self.on_export_dir)
        self.add_tag_btn.clicked.connect(self.on_add_tag)
        self.remove_tag_btn.clicked.connect(self.on_remove_tag)
        self.open_btn.clicked.connect(self.on_open)
        self.expr_input.returnPressed.connect(self.on_apply_filter)
        self.tag_list.tagsChanged.connect(self.on_apply_filter)
        self.tag_list.tagDeleteRequested.connect(self.on_delete_tag)
        self.tree.openRequested.connect(self._open_path)
        self.tree.renameRequested.connect(self.on_rename_pdf)
        self.tree.addTagsRequested.connect(self.on_add_tag_for_id)
        self.tree.removeTagsRequested.connect(self.on_remove_tag_for_id)
        self.tree.editDescriptionRequested.connect(self.on_edit_description)
        self.tree.viewDescriptionRequested.connect(self.on_view_description)
        self.tree.exportDirectoryRequested.connect(self.on_export_directory)
        self.tree.removeFromDbRequested.connect(self.on_remove_from_db)

        self.refresh_all()

    def closeEvent(self, event: QtGui.QCloseEvent) -> None:
        try:
            self.conn.close()
        except Exception:
            pass
        return super().closeEvent(event)

    def refresh_all(self):
        self.tag_list.refresh(self.conn)
        self.refresh_tree()

    def refresh_tree(self, ids: Optional[List[int]] = None):
        if ids is None:
            rows = list_pdfs(self.conn, limit=5000)
        else:
            rows = query_pdfs_by_ids(self.conn, ids)
        self.tree.populate_tree(rows)

    def _calc_filter_ids(self) -> Optional[List[int]]:
        expr = self.expr_input.text().strip()
        if expr:
            ids_expr = search_pdf_ids(self.conn, expr)
            return list(ids_expr)

        selected = self.tag_list.selected_tags()
        if not selected:
            return None
        mode = self.mode_combo.currentText().upper()
        # 使用数据库集合操作
        result_set = None
        for idx, name in enumerate(selected):
            ids = query_pdf_ids_by_tag(self.conn, name)
            if result_set is None:
                result_set = ids
            else:
                if mode == 'AND':
                    result_set = result_set & ids
                else:
                    result_set = result_set | ids
        return list(result_set or [])

    def on_apply_filter(self):
        try:
            ids = self._calc_filter_ids()
        except ValueError as err:
            QtWidgets.QMessageBox.warning(self, "筛选", f"表达式错误：{err}")
            return
        self.refresh_tree(ids)

    def on_scan(self):
        dlg = QtWidgets.QFileDialog(self)
        dlg.setFileMode(QtWidgets.QFileDialog.Directory)
        dlg.setOption(QtWidgets.QFileDialog.ShowDirsOnly, True)
        if dlg.exec() == QtWidgets.QDialog.Accepted:
            directory = dlg.selectedFiles()[0]
            progress = QtWidgets.QProgressDialog("扫描中...", "取消", 0, 0, self)
            progress.setWindowModality(QtCore.Qt.WindowModal)
            progress.show()

            def on_progress(msg: str):
                progress.setLabelText(msg)
                QtWidgets.QApplication.processEvents()
                if progress.wasCanceled():
                    raise RuntimeError("用户取消")

            try:
                scan_directory(self.db_path, directory, on_progress)
            except Exception as e:
                QtWidgets.QMessageBox.warning(self, "扫描", f"扫描中断: {e}")
            finally:
                progress.close()
                self.refresh_all()

    def on_export_dir(self):
        dlg = QtWidgets.QFileDialog(self)
        dlg.setFileMode(QtWidgets.QFileDialog.Directory)
        dlg.setOption(QtWidgets.QFileDialog.ShowDirsOnly, True)
        if dlg.exec() == QtWidgets.QDialog.Accepted:
            directory = dlg.selectedFiles()[0]
            self._export_directory_tags(directory)

    def on_export_directory(self, directory: str):
        if not os.path.isdir(directory):
            QtWidgets.QMessageBox.warning(self, "导出", "目录不存在或无法访问")
            return
        self._export_directory_tags(directory)

    def _export_directory_tags(self, directory: str):
        from src.manifest import export_directory_tags

        try:
            out_path = export_directory_tags(self.conn, directory)
            QtWidgets.QMessageBox.information(self, "导出", f"已导出到\n{out_path}")
        except Exception as e:
            QtWidgets.QMessageBox.warning(self, "导出", f"导出失败：{e}")

    def _selected_pdf_id(self) -> Optional[int]:
        info = self.tree.current_file_info()
        return info[0] if info else None

    def on_add_tag(self):
        pdf_id = self._selected_pdf_id()
        if not pdf_id:
            QtWidgets.QMessageBox.information(self, "标签", "请先选中一个 PDF")
            return
        text, ok = QtWidgets.QInputDialog.getText(self, "添加标签", "输入标签（多个用逗号分隔）：")
        if not ok or not text.strip():
            return
        tags = [t.strip() for t in text.split(',') if t.strip()]
        with self.conn:
            add_tags_for_pdf(self.conn, pdf_id, tags)
        self.refresh_all()

    def on_add_tag_for_id(self, pdf_ids: List[int]):
        # 处理多个PDF ID
        if not pdf_ids:
            return
        text, ok = QtWidgets.QInputDialog.getText(self, "添加标签", f"为 {len(pdf_ids)} 个PDF添加标签（多个用逗号分隔）：")
        if not ok or not text.strip():
            return
        tags = [t.strip() for t in text.split(',') if t.strip()]
        with self.conn:
            for pdf_id in pdf_ids:
                add_tags_for_pdf(self.conn, pdf_id, tags)
        self.refresh_all()

    def on_remove_tag(self):
        pdf_id = self._selected_pdf_id()
        if not pdf_id:
            QtWidgets.QMessageBox.information(self, "标签", "请先选中一个 PDF")
            return
        # Show current tags
        tags = get_pdf_tags(self.conn, pdf_id)
        if not tags:
            QtWidgets.QMessageBox.information(self, "标签", "该 PDF 目前没有标签")
            return
        items = ", ".join(tags)
        text, ok = QtWidgets.QInputDialog.getText(self, "移除标签", f"已有标签：{items}\n输入要移除的标签（逗号分隔）：")
        if not ok or not text.strip():
            return
        to_remove = [t.strip() for t in text.split(',') if t.strip()]
        with self.conn:
            remove_tags_for_pdf(self.conn, pdf_id, to_remove)
        self.refresh_all()

    def on_delete_tag(self, tag_name: str):
        if not tag_name:
            return
        if (
            QtWidgets.QMessageBox.question(
                self,
                "删除标签",
                f"确认删除标签“{tag_name}”吗？\n该标签将从所有文件中移除。",
            )
            != QtWidgets.QMessageBox.Yes
        ):
            return
        removed = delete_tag(self.conn, tag_name)
        if not removed:
            QtWidgets.QMessageBox.information(self, "删除标签", "未找到要删除的标签。")
            return
        self.refresh_all()

    def on_remove_tag_for_id(self, pdf_ids: List[int]):
        if not pdf_ids:
            return
        # 收集所有选中PDF的标签
        all_tags = set()
        pdf_tags = {}
        for pdf_id in pdf_ids:
            tags = get_pdf_tags(self.conn, pdf_id)
            pdf_tags[pdf_id] = tags
            all_tags.update(tags)
        
        if not all_tags:
            QtWidgets.QMessageBox.information(self, "标签", f"选中的 {len(pdf_ids)} 个PDF都没有标签")
            return
        
        items = ", ".join(sorted(all_tags))
        text, ok = QtWidgets.QInputDialog.getText(self, "移除标签", f"选中 {len(pdf_ids)} 个PDF\n共有标签：{items}\n输入要移除的标签（逗号分隔）：")
        if not ok or not text.strip():
            return
        to_remove = [t.strip() for t in text.split(',') if t.strip()]
        with self.conn:
            for pdf_id in pdf_ids:
                remove_tags_for_pdf(self.conn, pdf_id, to_remove)
        self.refresh_all()

    def on_open(self):
        info = self.tree.current_file_info()
        path = info[1] if info else None
        if not path or not os.path.exists(path):
            QtWidgets.QMessageBox.information(self, "打开", "文件不存在或未选择")
            return
        self._open_path(path)

    def _open_path(self, path: str):
        if not path or not os.path.exists(path):
            QtWidgets.QMessageBox.information(self, "打开", "文件不存在或未选择")
            return
        # Windows
        try:
            os.startfile(path)  # type: ignore[attr-defined]
            return
        except Exception:
            pass
        # macOS
        try:
            subprocess.run(["open", path], check=False)
            return
        except Exception:
            pass
        # Linux
        try:
            subprocess.run(["xdg-open", path], check=False)
        except Exception:
            QtWidgets.QMessageBox.warning(self, "打开", "无法启动外部程序打开文件")

    def on_rename_pdf(self, pdf_id: int, old_path: str):
        if not os.path.exists(old_path):
            QtWidgets.QMessageBox.warning(self, "重命名", "原文件不存在")
            return
        directory = os.path.dirname(old_path)
        old_name = os.path.basename(old_path)
        new_name, ok = QtWidgets.QInputDialog.getText(self, "重命名", f"当前：{old_name}\n输入新文件名：")
        if not ok or not new_name.strip():
            return
        if not new_name.lower().endswith('.pdf'):
            new_name = new_name + '.pdf'
        new_path = os.path.join(directory, new_name)
        if os.path.exists(new_path):
            QtWidgets.QMessageBox.warning(self, "重命名", "目标文件已存在")
            return
        try:
            os.rename(old_path, new_path)
        except Exception as e:
            QtWidgets.QMessageBox.warning(self, "重命名", f"失败：{e}")
            return
        with self.conn:
            add_or_update_path(self.conn, pdf_id, new_path)
        self.refresh_all()

    def on_remove_from_db(self, pdf_id: int):
        if QtWidgets.QMessageBox.question(self, "从库移除", "仅从数据库移除，不删除文件。确定？") != QtWidgets.QMessageBox.Yes:
            return
        with self.conn:
            self.conn.execute("DELETE FROM pdfs WHERE id = ?", (pdf_id,))
        self.refresh_all()

    def on_edit_description(self, pdf_ids: List[int]):
        if not pdf_ids:
            return
        
        # 获取第一个选中PDF的当前描述作为默认值
        first_id = pdf_ids[0]
        current_desc = ""
        try:
            row = self.conn.execute("SELECT description FROM pdfs WHERE id = ?", (first_id,)).fetchone()
            if row and row[0]:
                current_desc = row[0]
        except Exception:
            pass
        
        title = f"编辑描述（{len(pdf_ids)} 个PDF）" if len(pdf_ids) > 1 else "编辑描述"
        text, ok = QtWidgets.QInputDialog.getMultiLineText(self, title, "输入描述：", current_desc)
        if not ok:
            return
        
        description = text.strip() if text.strip() else None
        with self.conn:
            for pdf_id in pdf_ids:
                update_pdf_description(self.conn, pdf_id, description)
        self.refresh_all()

    def on_view_description(self, pdf_id: int):
        description = ""
        path = ""

        item = self.tree.get_item_by_id(pdf_id)
        if item is not None:
            description = item.data(2, QtCore.Qt.UserRole) or ""
            info = self.tree._get_file_info_by_id(pdf_id)
            if info:
                _, path = info

        if not description:
            try:
                row = self.conn.execute(
                    "SELECT description, path FROM pdfs WHERE id = ?",
                    (pdf_id,),
                ).fetchone()
            except Exception:
                row = None
            else:
                if row:
                    description = row[0] or ""
                    path = path or (row[1] or "")

        if not description:
            QtWidgets.QMessageBox.information(self, "描述", "暂无描述")
            return

        title = "完整描述"
        if path:
            title = f"{title} - {os.path.basename(path)}"

        dialog = QtWidgets.QDialog(self)
        dialog.setWindowTitle(title)
        dialog.resize(640, 480)

        layout = QtWidgets.QVBoxLayout(dialog)
        browser = QtWidgets.QTextBrowser(dialog)
        browser.setReadOnly(True)
        browser.setOpenExternalLinks(True)
        browser.setHtml(markdown_to_html(description))
        layout.addWidget(browser)

        buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Close)
        buttons.rejected.connect(dialog.reject)
        layout.addWidget(buttons)

        dialog.exec()

    # 点击标签由复选框交互替代，无需实现点击即查询


def run_gui(db_path: str):
    app = QtWidgets.QApplication([])
    win = MainWindow(db_path)
    win.show()
    app.exec()


